﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MugenMvvmToolkit.Infrastructure;
using MugenMvvmToolkit.Interfaces;
using MugenMvvmToolkit.Interfaces.ViewModels;
using MugenMvvmToolkit.Models;
using MugenMvvmToolkit.ViewModels;
using MugenMvvmToolkitTest.Infrastructure;
using MugenMvvmToolkitTest.TestInfrastructure;
using MugenMvvmToolkitTest.TestModels;
using Should;

namespace MugenMvvmToolkitTest.ViewModel
{
    [TestClass]
    public class ValidatableViewModelTest : TestBase
    {
        #region Fields

        public const string PropToValidate1 = "PropToValidate1";
        public const string PropToValidate2 = "PropToValidate2";

        #endregion

        #region Properties

        protected TestValidationProvider ValidationProvider { get; set; }

        protected ValidatorProvider ValidatorProvider { get; set; }

        #endregion

        #region Test methods

        [TestMethod]
        public void VmShouldValidateSelfByDefault()
        {
            ValidatorProvider.Register<SpyValidator>();
            var validatableViewModel = GetValidatableViewModel();
            var validator = (SpyValidator)validatableViewModel.Validators.Single();
            validator.Context.InstanceToValidate.ShouldEqual(validatableViewModel);
        }

        [TestMethod]
        public void CreateContextShouldCreateContextWithDefaultValuesFromVm()
        {
            var metadata = new Dictionary<object, object>();
            var obj = new object();
            var viewModel = GetValidatableViewModel();
            viewModel.Settings.MetadataFactory = () => metadata;

            var validatorContext = viewModel.CreateContext(obj);
            validatorContext.ServiceProvider.ShouldEqual(viewModel.IocAdapter);
            validatorContext.ValidationMetadata.ShouldEqual(metadata);
            validatorContext.PropertiesMapping.ShouldEqual(viewModel.PropertiesMapping);
            validatorContext.IgnoreProperties.ShouldEqual(viewModel.IgnoreProperties);
            validatorContext.InstanceToValidate.ShouldEqual(obj);
            validatorContext.ServiceProvider.ShouldEqual(viewModel.IocAdapter);
        }

        [TestMethod]
        public void AddNotInitializedValidatorShouldThrowException()
        {
            var viewModel = GetValidatableViewModel();
            ShouldThrow<ArgumentException>(() => viewModel.AddValidator(new SpyValidator()));
        }

        [TestMethod]
        public void RemoveNotInitializedValidatorShouldThrowException()
        {
            var viewModel = GetViewModel<ValidatableViewModel>();
            ShouldThrow<ArgumentException>(() => viewModel.RemoveValidator(new SpyValidator()));
        }

        [TestMethod]
        public void AddedValidatorGenericShouldBeInValidators()
        {
            var o = new object();
            var viewModel = GetValidatableViewModel();
            var validator = viewModel.AddValidator<SpyValidator>(o);
            validator.IsInitialized.ShouldBeTrue();
            validator.Context.InstanceToValidate.ShouldEqual(o);
            viewModel.Validators.Single().ShouldEqual(validator);
        }

        [TestMethod]
        public void AddedValidatorShouldBeInValidators()
        {
            var o = new object();
            var viewModel = GetValidatableViewModel();
            var validator = new SpyValidator();
            validator.Initialize(viewModel.CreateContext(o));
            viewModel.AddValidator(validator);
            validator.IsInitialized.ShouldBeTrue();
            validator.Context.InstanceToValidate.ShouldEqual(o);
            viewModel.Validators.Single().ShouldEqual(validator);
        }

        [TestMethod]
        public void RemoveValidatorGenericShouldBeRemoveFromValidators()
        {
            var o = new object();
            var viewModel = GetValidatableViewModel();
            var validator = viewModel.AddValidator<SpyValidator>(o);
            viewModel.Validators.Single().ShouldEqual(validator);
            viewModel.RemoveValidator(validator).ShouldBeTrue();
            viewModel.Validators.ShouldBeEmpty();
        }

        [TestMethod]
        public void AddInstanceShouldGetValidatorsFromProviderEmpty()
        {
            var viewModel = GetValidatableViewModel();
            var instance = new object();
            viewModel.AddInstance(instance);
            viewModel.Validators.ShouldBeEmpty();
        }

        [TestMethod]
        public void AddInstanceShouldGetValidatorsFromProviderNotEmpty()
        {
            var viewModel = GetValidatableViewModel();
            ValidatorProvider.Register<SpyValidator>();
            var instance = new object();
            viewModel.AddInstance(instance);
            viewModel.Validators.Single().Context.InstanceToValidate.ShouldEqual(instance);
        }

        [TestMethod]
        public void RemoveInstanceShouldRemoveAllValidatorsAssociatedWithInstance()
        {
            var viewModel = GetValidatableViewModel();
            ValidatorProvider.Register<SpyValidator>();
            var instance = new object();
            var instance2 = new object();
            viewModel.AddInstance(instance);
            viewModel.AddInstance(instance2);
            viewModel.AddValidator<SimpleValidator>(instance);
            viewModel.Validators.Count().ShouldEqual(3);

            viewModel.RemoveInstance(instance);
            viewModel.Validators.Single().Context.InstanceToValidate.ShouldEqual(instance2);
        }

        [TestMethod]
        public void ValidateShouldRedirectCallToValidators()
        {
            const int count = 10;
            var viewModel = GetValidatableViewModel();
            ValidatorProvider.Register<SpyValidator>();

            for (int i = 0; i < count; i++)
            {
                viewModel.AddInstance(new object());
            }
            viewModel.Validators.Count().ShouldEqual(count);
            viewModel.Validate(PropToValidate1);
            viewModel.Validators.Cast<SpyValidator>().All(validator => validator.ValidateProperties.Contains(PropToValidate1)).ShouldBeTrue();
            viewModel.Validators.Cast<SpyValidator>().All(validator => validator.ValidateCount == 1).ShouldBeTrue();
        }

        [TestMethod]
        public void ValidateAllShouldRedirectCallToValidators()
        {
            const int count = 10;
            var viewModel = GetValidatableViewModel();
            ValidatorProvider.Register<SpyValidator>();

            for (int i = 0; i < count; i++)
            {
                viewModel.AddInstance(new object());
            }
            viewModel.Validators.Count().ShouldEqual(count);
            viewModel.Validators.Cast<SpyValidator>().All(validator => validator.ValidateAllCount == 1).ShouldBeTrue();
        }

        [TestMethod]
        public void ValidateInstanceShouldValidateOnlyInstance()
        {
            var viewModel = GetValidatableViewModel();
            ValidatorProvider.Register<SpyValidator>();
            var instance = new object();
            var instance2 = new object();
            viewModel.AddInstance(instance);
            viewModel.AddInstance(instance2);
            viewModel.AddValidator<SpyValidator>(instance);
            viewModel.Validators.Count().ShouldEqual(3);
            viewModel.Validators.Cast<SpyValidator>().ForEach(validator => validator.ValidateAllCount = 0);
            viewModel.ValidateInstance(instance);

            foreach (var result in viewModel.Validators.Cast<SpyValidator>())
            {
                if (result.Context.InstanceToValidate == instance)
                    result.ValidateAllCount.ShouldEqual(1);
                else
                    result.ValidateAllCount.ShouldEqual(0);
            }
        }

        [TestMethod]
        public void IsValidShouldRedirectCallToValidators()
        {
            var viewModel = GetValidatableViewModel();
            var validator1 = viewModel.AddValidator<SpyValidator>(new object());
            var validator2 = viewModel.AddValidator<SpyValidator>(new object());

            viewModel.IsValid().ShouldBeTrue();
            validator1.SetErrors(PropToValidate1, PropToValidate1);
            validator2.SetErrors(PropToValidate2, PropToValidate2);
            viewModel.IsValid().ShouldBeFalse();

            validator1.SetErrors(PropToValidate1);
            viewModel.IsValid().ShouldBeFalse();

            validator2.SetErrors(PropToValidate2);
            viewModel.IsValid().ShouldBeTrue();
        }

        [TestMethod]
        public void GetErrorsShouldRedirectCallToValidators()
        {
            var viewModel = GetValidatableViewModel();
            var validator1 = viewModel.AddValidator<SpyValidator>(new object());
            var validator2 = viewModel.AddValidator<SpyValidator>(new object());

            viewModel.GetErrors(PropToValidate1).ShouldBeEmpty();
            viewModel.GetErrors(PropToValidate2).ShouldBeEmpty();

            validator1.SetErrors(PropToValidate1, PropToValidate1);
            validator1.SetErrors(PropToValidate2, PropToValidate2);
            validator2.SetErrors(PropToValidate1, PropToValidate2);
            validator2.SetErrors(PropToValidate2, PropToValidate1);

            var errors = viewModel.GetErrors(PropToValidate1).OfType<object>().ToArray();
            errors.Length.ShouldEqual(2);
            errors.Contains(PropToValidate1).ShouldBeTrue();
            errors.Contains(PropToValidate2).ShouldBeTrue();

            errors = viewModel.GetErrors(PropToValidate2).OfType<object>().ToArray();
            errors.Length.ShouldEqual(2);
            errors.Contains(PropToValidate1).ShouldBeTrue();
            errors.Contains(PropToValidate2).ShouldBeTrue();

            validator1.SetErrors(PropToValidate1);
            validator1.SetErrors(PropToValidate2);

            errors = viewModel.GetErrors(PropToValidate1).OfType<object>().ToArray();
            errors.Length.ShouldEqual(1);
            errors.Contains(PropToValidate2).ShouldBeTrue();

            errors = viewModel.GetErrors(PropToValidate2).OfType<object>().ToArray();
            errors.Length.ShouldEqual(1);
            errors.Contains(PropToValidate1).ShouldBeTrue();

            validator2.SetErrors(PropToValidate1);
            validator2.SetErrors(PropToValidate2);

            viewModel.GetErrors(PropToValidate1).ShouldBeEmpty();
            viewModel.GetErrors(PropToValidate2).ShouldBeEmpty();
        }

        [TestMethod]
        public void GetAllErrorsShouldRedirectCallToValidators()
        {
            var viewModel = GetValidatableViewModel();
            var validator1 = viewModel.AddValidator<SpyValidator>(new object());
            var validator2 = viewModel.AddValidator<SpyValidator>(new object());

            viewModel.GetAllErrors().ShouldBeEmpty();
            viewModel.GetAllErrors().ShouldBeEmpty();

            validator1.SetErrors(PropToValidate1, PropToValidate1);
            validator1.SetErrors(PropToValidate2, PropToValidate2);
            validator2.SetErrors(PropToValidate1, PropToValidate2);
            validator2.SetErrors(PropToValidate2, PropToValidate1);

            var errors = viewModel.GetAllErrors();
            errors.Count.ShouldEqual(2);

            errors[PropToValidate1].Count.ShouldEqual(2);
            errors[PropToValidate1].Contains(PropToValidate1).ShouldBeTrue();
            errors[PropToValidate1].Contains(PropToValidate2).ShouldBeTrue();

            errors[PropToValidate2].Count.ShouldEqual(2);
            errors[PropToValidate2].Contains(PropToValidate1).ShouldBeTrue();
            errors[PropToValidate2].Contains(PropToValidate2).ShouldBeTrue();

            validator1.SetErrors(PropToValidate1);
            validator1.SetErrors(PropToValidate2);

            errors = viewModel.GetAllErrors();
            errors.Count.ShouldEqual(2);

            errors[PropToValidate1].Count.ShouldEqual(1);
            errors[PropToValidate1].Contains(PropToValidate2).ShouldBeTrue();

            errors[PropToValidate2].Count.ShouldEqual(1);
            errors[PropToValidate2].Contains(PropToValidate1).ShouldBeTrue();

            validator2.SetErrors(PropToValidate1);
            validator2.SetErrors(PropToValidate2);

            viewModel.GetAllErrors().ShouldBeEmpty();
            viewModel.GetAllErrors().ShouldBeEmpty();
        }

        [TestMethod]
        public void ClearErrorsPropertyShouldRedirectCallToValidators()
        {
            var viewModel = GetValidatableViewModel();
            var validator1 = viewModel.AddValidator<SpyValidator>(new object());
            var validator2 = viewModel.AddValidator<SpyValidator>(new object());

            viewModel.ClearErrors(PropToValidate1);
            validator1.ClearPropertyErrorsCount.ShouldEqual(1);
            validator2.ClearPropertyErrorsCount.ShouldEqual(1);
        }


        [TestMethod]
        public void ClearErrorsShouldRedirectCallToValidators()
        {
            var viewModel = GetValidatableViewModel();
            var validator1 = viewModel.AddValidator<SpyValidator>(new object());
            var validator2 = viewModel.AddValidator<SpyValidator>(new object());

            viewModel.ClearErrors();
            validator1.ClearAllErrorsCount.ShouldEqual(2);
            validator2.ClearAllErrorsCount.ShouldEqual(2);
        }

#if !NODATAERROR
        [TestMethod]
        public void DataErrorInfoShouldGetErrorsFromValidators()
        {
            var viewModel = GetValidatableViewModel();
            IDataErrorInfo dataErrorInfo = viewModel;

            var validator = viewModel.AddValidator<SpyValidator>(new object());
            validator.SetErrors(PropToValidate1, PropToValidate1);

            string error = dataErrorInfo[PropToValidate1];
            validator.ValidateCount.ShouldEqual(1);
            validator.ValidateProperties.Contains(PropToValidate1).ShouldBeTrue();
            error.ShouldEqual(PropToValidate1);

            validator.SetErrors(PropToValidate1);
            error = validator[PropToValidate1];
            validator.ValidateCount.ShouldEqual(2);
            error.ShouldBeNull();
        }
#endif

#if !NONOTIFYDATAERROR
        [TestMethod]
        public void NotifyDataErrorInfoShouldGetErrorsFromValidators()
        {
            var viewModel = GetValidatableViewModel();
            INotifyDataErrorInfo notifyDataError = viewModel;

            var validator = viewModel.AddValidator<SpyValidator>(new object());

            notifyDataError.HasErrors.ShouldBeFalse();
            validator.SetErrors(PropToValidate1, PropToValidate1, PropToValidate2);
            notifyDataError.HasErrors.ShouldBeTrue();

            string[] errors = notifyDataError.GetErrors(PropToValidate1).OfType<string>().ToArray();
            errors.Length.ShouldEqual(2);
            errors.Contains(PropToValidate1).ShouldBeTrue();
            errors.Contains(PropToValidate2).ShouldBeTrue();

            validator.SetErrors(PropToValidate1);
            notifyDataError.GetErrors(PropToValidate1).ShouldBeEmpty();
            notifyDataError.HasErrors.ShouldBeFalse();
        }

        [TestMethod]
        public void ValidateMethodShouldCallNotifyEvent()
        {
            ThreadManager.ImmediateInvokeAsync = true;
            ThreadManager.ImmediateInvokeInUi = true;
            ThreadManager.ImmediateInvokeInUiSynchronous = true;

            int countInvoke = 0;
            var viewModel = GetValidatableViewModel();
            INotifyDataErrorInfo notifyDataError = viewModel;
            notifyDataError.ErrorsChanged += (sender, args) =>
            {
                args.PropertyName.ShouldEqual(PropToValidate1);
                countInvoke++;
            };
            var validator = viewModel.AddValidator<SpyValidator>(new object());

            notifyDataError.HasErrors.ShouldBeFalse();
            validator.SetErrors(PropToValidate1, PropToValidate1);
            countInvoke.ShouldEqual(1);
            validator.SetErrors(PropToValidate1, PropToValidate2);
            countInvoke.ShouldEqual(2);
            notifyDataError.HasErrors.ShouldBeTrue();

            viewModel.Validate(PropToValidate1);
            countInvoke.ShouldEqual(3);
        }
#endif
        #endregion

        #region Methods

        protected virtual ValidatableViewModel GetValidatableViewModel()
        {
            return GetViewModel<ValidatableViewModel>();
        }

        #endregion

        #region Overrides of TestBase

        protected override object GetFunc(Type type, string s, IIocParameter[] arg3)
        {
            if (type == typeof(IValidatorProvider))
                return ValidatorProvider;
            if (type == typeof(IValidationProvider))
                return ValidationProvider;
            return base.GetFunc(type, s, arg3);
        }

        protected override void OnInit()
        {
            ValidationProvider = new TestValidationProvider();
            ValidatorProvider = new ValidatorProvider(IocAdapter);
        }

        #endregion
    }
}